package org.deltaset.meta.internal;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.deltaset.meta.annotation.DsFirstResult;
import org.deltaset.meta.annotation.DsMaxResult;

/**
 *
 * @author dmakariev
 */
class ReflectionUtils {
////////methods

    private static final Logger logger = Logger.getLogger(ReflectionUtils.class.getName());

    static List<Method> findActions(final Class clazz) {
        return filterMethods(clazz, new Filter<Method>() {

            @Override
            public boolean isSatisfied(final Method method) {
                return isAction(method);
            }
        });
    }

    interface Filter<T> {

        boolean isSatisfied(final T object);
    }

    private static List<Method> filterMethods(final Class clazz, final Filter<Method> filter) {
        if (null == clazz) {
            return Collections.emptyList();
        }
        final List<Method> foundMethods = new ArrayList<Method>();
        final Method[] methods = clazz.getMethods(); //all public methods, including inherited,so - NO need from recursion..
        for (int i = 0; i < methods.length; i++) {
            final Method method = methods[i];
            if (true == filter.isSatisfied(method)) {
                foundMethods.add(method);
            }
        }
        return Collections.unmodifiableList(foundMethods);
    }

    //should not start with 'get', 'set' or 'is' 
    static boolean isAction(final Method method) {
        final String methodName = method.getName();
        final boolean isGetter = methodName.startsWith("get") || methodName.startsWith("is");
        final boolean isSetter = methodName.startsWith("set");
        final boolean isEqualsOrHashCode = methodName.equals("equals") || methodName.equals("hashCode");
        final boolean isReadResolve = methodName.equals("readResolve"); //comming from spring's @Configurable aspect 

        //TODO: think !
        final boolean isObjectMember = method.getDeclaringClass().equals(java.lang.Object.class);

        //is bridge - automatically generated from the compiler to support generics
        final boolean isBridge = method.isBridge();
        //is bridge - automatically generated from the compiler to support generics
        final boolean isSynthetic = method.isSynthetic();

        final boolean isAction = !isGetter && !isSetter && !isObjectMember && !isEqualsOrHashCode && !isReadResolve
                && !isBridge
                && !isSynthetic;
        return isAction;
    }
////////////fields

    static List<Field> findFieldByAnnotation(final Class clazz, final Class annotationClass) {
        final List<Field> foundFields = new ArrayList<Field>();
        filterFields(foundFields, clazz, new Filter<Field>() {

            @Override
            public boolean isSatisfied(Field field) {
                final Annotation annotation = field.getAnnotation(annotationClass);
                if (null != annotation) {
                    field.setAccessible(true);
                    return true;
                }
                return false;
            }
        });

        if (foundFields.isEmpty()) {
            throw new RuntimeException("Cannot find field for class=" + clazz + " with annotation=" + annotationClass);
        }

        return Collections.unmodifiableList(foundFields);
    }

    static List<BeanProperty> findBeanProperties(final Class clazz) {
        final List<BeanProperty> beanProperties = findBeanPropertyFromFields(clazz);

        //adding support for delegate property methods:
        beanProperties.addAll(findDelegateBeanProperties(clazz));

        return beanProperties;
    }

    static List<BeanProperty> findDelegateBeanProperties(final Class clazz) {
        final List<BeanProperty> beanProperties = new ArrayList<BeanProperty>();
        final Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            final boolean isPublic = Modifier.isPublic(method.getModifiers());
            final Class returnType = method.getReturnType();
            final Type genericType = method.getGenericReturnType();
            final boolean isNotReturningVoid = !Void.TYPE.equals(returnType);
            if (isPublic && isNotReturningVoid) {
                final String methodName = method.getName();
                final String propertyName = extractPropertyName(methodName);
                final boolean doesNotHaveField = !hasField(clazz, propertyName);
                if (doesNotHaveField && isBeanProperty(propertyName, returnType, clazz)) {
                    final Annotation[] fieldAnnotations = extractDelegateBeanPropertyAnnotations(propertyName, returnType, clazz);
                    beanProperties.add(new BeanProperty(propertyName, returnType, genericType, fieldAnnotations));
                }
            }
        }
        return beanProperties;
    }

    private static Annotation[] extractDelegateBeanPropertyAnnotations(final String propertyName, final Class returnType, final Class clazz) {

        final Set<Annotation> annotations = new HashSet<Annotation>();

        final String isName = prefixedProperty("is", propertyName);
        final Method isMethod = getMethod(clazz, isName);
        if (null != isMethod) {
            annotations.addAll(Arrays.asList(isMethod.getAnnotations()));
        }

        final String getName = prefixedProperty("get", propertyName);
        final Method getMethod = getMethod(clazz, getName);
        if (null != getMethod) {
            annotations.addAll(Arrays.asList(getMethod.getAnnotations()));
        }

        final String setName = prefixedProperty("set", propertyName);
        final Method setMethod = getMethod(clazz, setName, returnType);
        if (null != getMethod) {
            annotations.addAll(Arrays.asList(setMethod.getAnnotations()));
        }

        return annotations.toArray(new Annotation[annotations.size()]);
    }

    //TODO: check, possible issue with ordering of bean properties between versions of jdk. 
    //TODO: should probvide some 'natural' ordering, which is predictable
    static List<BeanProperty> findBeanPropertyFromFields(final Class clazz) {
        final List<Field> foundFields = findFieldsIfPropertyOrCollection(clazz);
        final List<BeanProperty> beanPropertys = new ArrayList<BeanProperty>();
        for (Field field : foundFields) {
            beanPropertys.add(new BeanProperty(field));
        }
        return beanPropertys;
    }

    private static List<Field> findFieldsIfPropertyOrCollection(final Class clazz) {
        final List<Field> foundFields = new ArrayList<Field>();
        filterFields(foundFields, clazz, new Filter<Field>() {

            @Override
            public boolean isSatisfied(final Field field) {
                return isProperty(clazz, field) || isCollection(clazz, field);
            }
        });
        return Collections.unmodifiableList(foundFields);
    }

    static List<Field> findFieldsIfProperty(final Class clazz) {
        final List<Field> foundFields = new ArrayList<Field>();
        filterFields(foundFields, clazz, new Filter<Field>() {

            @Override
            public boolean isSatisfied(final Field field) {
                return isProperty(clazz, field);
            }
        });
        return Collections.unmodifiableList(foundFields);
    }

    private static void filterFields(final List<Field> foundFields, final Class clazz, final Filter<Field> filter) {
        if (null == clazz) {
            return;
        }
        final Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            final Field field = fields[i];
            if (true == filter.isSatisfied(field)) {
                foundFields.add(field);
            }
        }
        filterFields(foundFields, clazz.getSuperclass(), filter);
    }

    /**
     * returns the index of a parameter that has declared annotation;
     */
    static int indexOf(final Method method, final Class annotationClass) {
        final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            final Annotation[] annotations = parameterAnnotations[i];
            for (int j = 0; j < annotations.length; j++) {
                final Annotation annotation = annotations[j];
                if (annotation.annotationType().equals(annotationClass)) {
                    return i;
                }
            }
        }
        return -1;
    }

    private static boolean isProperty(final Class mainClass, final Field field) {
        final String fieldName = field.getName();
        final Class fieldType = field.getType();
        return isBeanProperty(fieldName, fieldType, mainClass);
    }

    private static boolean hasField(final Class mainClass, final String fieldName) {
        if (null == fieldName || null == mainClass) {
            return false;
        }
        try {
            final Field field = mainClass.getDeclaredField(fieldName);
            return true;
        } catch (NoSuchFieldException ex) {
            //Logger.getLogger(ReflectionUtils.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            //Logger.getLogger(ReflectionUtils.class.getName()).log(Level.SEVERE, null, ex);
        }
        return hasField(mainClass.getSuperclass(), fieldName);
    }

    private static boolean isBeanProperty(final String fieldName, final Class fieldType, final Class mainClass) {
        if (null == fieldName) {
            return false;
        }
        final boolean hasGetter = hasGetter(mainClass, fieldName, fieldType);
        final boolean hasSetter = hasSetter(mainClass, fieldName, fieldType);
        return hasGetter && hasSetter;
    }

    private static boolean isCollection(final Class mainClass, final Field field) {
        final String fieldName = field.getName();
        final Class fieldType = field.getType();
        final boolean hasGetter = hasGetter(mainClass, fieldName, fieldType);
        final boolean isCollection = Collection.class.isAssignableFrom(fieldType);
        return hasGetter && isCollection;
    }

    private static boolean hasGetter(final Class mainClass, final String fieldName, final Class fieldType) {
        final boolean isBoolean = fieldType.equals(Boolean.class) || fieldType.equals(boolean.class);
        final String getterName = prefixedProperty("get", fieldName);
        //TODO: think again !.. probably all which exist should be allowed
        if (excludedGetters.contains(getterName)) {
            return false;
        }
        final boolean hasGetter = hasMethod(mainClass, fieldType, getterName);
        if (false == hasGetter && true == isBoolean) {
            final String isName = prefixedProperty("is", fieldName);
            return hasMethod(mainClass, fieldType, isName);
        }
        return hasGetter;
    }

    private static boolean hasSetter(final Class mainClass, final String fieldName, final Class fieldType) {
        final String setterName = prefixedProperty("set", fieldName);
        return hasMethod(mainClass, Void.TYPE, setterName, fieldType);
    }

    static boolean hasMethod(final Class mainClass, final String methodName, final Class... parameters) {
        return null != getMethod(mainClass, methodName, parameters);
    }

    static Method getMethod(final Class mainClass, final String methodName, final Class... parameters) {
        try {
            final Method method = mainClass.getMethod(methodName, parameters);
            return method;
        } catch (NoSuchMethodException ex) {
        } catch (SecurityException ex) {
        }
        return null;
    }

    static boolean hasMethod(final Class mainClass, final Class returnType, final String methodName, final Class... parameters) {
        try {
            final Method method = mainClass.getMethod(methodName, parameters);
            return method.getReturnType().equals(returnType);
        } catch (NoSuchMethodException ex) {
        } catch (SecurityException ex) {
        }
        return false;
    }

    static Object execute(final Method method, final Object mainObject, final Object... parameters) {
        try {
            final Method realMethod = findProxiedMethod(method, mainObject);
            final Object[] realParameters = initParameters(realMethod, parameters);
            return realMethod.invoke(mainObject, realParameters);
        } catch (NoSuchMethodException ex) {
            logger.log(Level.INFO, "ex:", ex);
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        } catch (IllegalAccessException ex) {
            logger.log(Level.INFO, "ex:", ex);
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        } catch (IllegalArgumentException ex) {
            logger.log(Level.INFO, "ex:", ex);
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        } catch (InvocationTargetException ex) {
            logger.log(Level.INFO, "ex:", ex);
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        }
    }

    private static Object[] initParameters(final Method realMethod, final Object[] parameters) {
        final int parameterSize = realMethod.getParameterTypes().length;
        if (null != parameters && parameterSize == parameters.length) {
            return parameters;
        }
        final Object[] realParameters = new Object[parameterSize];
        if (null != parameters) {
            for (int i = 0; i < parameters.length; i++) {
                final Object object = parameters[i];
                realParameters[i] = object;

            }
        }
        return realParameters;
    }

    private static Method findProxiedMethod(final Method method, final Object mainObject) throws NoSuchMethodException {
        final boolean isTheSameClass = method.getDeclaringClass().equals(mainObject.getClass());
        if (true == isTheSameClass) {
            return method;
        }

        final String methodName = method.getName();
        final Class[] parameterTypes = method.getParameterTypes();
        return mainObject.getClass().getMethod(methodName, parameterTypes);
    }

    static Object executeMethod(final Object mainObject, final String methodName, final Object... parameters) {
        final Class[] parameterClasses = objectToClass(parameters);
        final Class mainClass = mainObject.getClass();
        try {
            final Method method = mainClass.getMethod(methodName, parameterClasses);

            return method.invoke(mainObject, parameters);

        } catch (NoSuchMethodException ex) {
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        } catch (SecurityException ex) {
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        } catch (InvocationTargetException ex) {
            throw new RuntimeException("executeMethod " + ex.toString(), ex);
        }
    }

    static Method extractDependantContextMethod(final String prefix, final Method method, final Class entityClass) {
        final Class declaringClass = method.getDeclaringClass();
        final boolean isEntityMember = entityClass.equals(declaringClass);
        final String dependantMethodName = ReflectionUtils.prefixedProperty(prefix, method.getName());
        if (true == isEntityMember) {
            final Method dependantMethod = ReflectionUtils.getMethod(entityClass, dependantMethodName);
            return dependantMethod;
        } else {
            final Method acceptingEntityMethod = ReflectionUtils.getMethod(declaringClass, dependantMethodName, entityClass);
            if (null == acceptingEntityMethod) {
                final Method acceptingObjectMethod = ReflectionUtils.getMethod(declaringClass, dependantMethodName, Object.class);
                return acceptingObjectMethod;
            }
            return acceptingEntityMethod;
        }
    }

    private static Class[] objectToClass(Object[] objects) {
        if (null == objects) {
            return null;
        }
        final Class[] parameterClasses = new Class[objects.length];
        for (int i = 0; i < objects.length; i++) {
            final Class clazz = objects[i].getClass();
            parameterClasses[i] = clazz;
        }
        return parameterClasses;
    }

    private static String extractPropertyName(final String methodName) {
        if (methodName.startsWith("get") && methodName.length() > 3) {
            return methodName.substring(3, 4).toLowerCase() + (methodName.length() > 4 ? methodName.substring(4) : "");
        }
        if (methodName.startsWith("is") && methodName.length() > 2) {
            return methodName.substring(2, 3).toLowerCase() + (methodName.length() > 3 ? methodName.substring(3) : "");
        }
        return null;
    }

    private static String prefixedProperty(final String prefix, final String propertyName) {
        final String result = prefix + propertyName.substring(0, 1).toUpperCase() + (propertyName.length() > 1 ? propertyName.substring(1) : "");
        return result;
    }
    private final static Set<String> excludedGetters;

    static {
        excludedGetters = new HashSet<String>();
        excludedGetters.add("getClass");
        excludedGetters.add("getId");
        excludedGetters.add("getVersion");
    }

    static Class loadClass(final String className) {
        try {
            final Class loadedClass = Class.forName(className);
            return loadedClass;
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException("Cannot load class:" + className, ex);
        }
    }

    static <T> T newInstance(Class<T> currentClass) {
        try {
            final T newEntity = currentClass.newInstance();
            return newEntity;
        } catch (InstantiationException ex) {
            throw new RuntimeException("could not create " + currentClass.getName(), ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException("could not create " + currentClass.getName(), ex);
        }
    }

    static boolean isPagedFinder(final Method method) {
        final int indexFirstResult = ReflectionUtils.indexOf(method, DsFirstResult.class);
        final int indexMaxResult = ReflectionUtils.indexOf(method, DsMaxResult.class);
        final boolean isPagedFinder = (-1 != indexFirstResult) && (-1 != indexMaxResult);
        return isPagedFinder;
    }
    private final static Set primitiveClassSet = new HashSet<Class>();

    static {
        primitiveClassSet.add(int.class);
        primitiveClassSet.add(Integer.class);
        primitiveClassSet.add(long.class);
        primitiveClassSet.add(Long.class);
        primitiveClassSet.add(float.class);
        primitiveClassSet.add(Float.class);
        primitiveClassSet.add(double.class);
        primitiveClassSet.add(Double.class);
        primitiveClassSet.add(short.class);
        primitiveClassSet.add(Short.class);
        primitiveClassSet.add(byte.class);
        primitiveClassSet.add(Byte.class);
        ///////
        primitiveClassSet.add(String.class);
        ///////
        primitiveClassSet.add(boolean.class);
        primitiveClassSet.add(Boolean.class);
        ///////
        primitiveClassSet.add(java.util.Date.class);
    }

    static boolean isPrimitiveType(final Class checkClass) {
        return primitiveClassSet.contains(checkClass);
    }

    static boolean hasDefaultConstructor(final Class someClass) {
        try {
            someClass.getConstructor();
            return true;
        } catch (NoSuchMethodException ex) {
        } catch (SecurityException ex) {
        }
        return false;
    }

    static boolean isSupportedParameterClass(Class parameterClass, boolean isEntity) {
        return parameterClass.isEnum() || isPrimitiveType(parameterClass) || isEntity;
    }

    static Class extractGenericClass(Type type) {
        if (type instanceof ParameterizedType) {
            final ParameterizedType parameterizedType = (ParameterizedType) type;
            final Class<?> typeClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
            return typeClass;
        }
        return null;
    }
}
